package com.ap.core.domain.sequence.generation.impl;

import com.ap.core.dao.SequenceGenerationDao;
import com.ap.core.domain.sequence.generation.IdGenerationException;
import com.ap.core.domain.sequence.generation.MultiIdGenerationService;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Implementation of {@link MultiIdGenerationService} based on database sequences. The service supports getting sequence in batches.
 * NOTE: The batch interval on the sequence configured in the database should be the same as the batch size on the service.
 * Second option: Multiple id generators are managed in one service and the batch size id inferred. The qualifier for the generator can be any arbitrary string value.
 */
public class MultiSequenceIdGenerationService implements MultiIdGenerationService<Long> {

    private SequenceGenerationDao sequenceGenerationDao;

    /** Captures the relationship between the class for a persistent entity and corresponding sequence for generating unique IDs for it */
    protected Properties qualifiers;

    /** Map that stores an {@link com.ap.core.domain.sequence.generation.impl.MultiSequenceIdGenerationService.IdGenerator} for each persistent entity */
    private final Map<Object, IdGenerator> idGenerators = new HashMap<Object, IdGenerator>();

    /**
     * Initialises the sequence generators.
     */
    public void init() {
        for (final Object qualifier : qualifiers.keySet()) {
            idGenerators.put(qualifier.toString().trim(), new IdGenerator(qualifiers.get(qualifier).toString().trim()));
        }
    }

    /**
     * {@inheritDoc}
     */
    public Long getNextId(final String qualifier) throws IdGenerationException {
        return idGenerators.get(qualifier).getNextId();
    }

    /**
     * {@inheritDoc}
     */
    public Long getNextId(final Class<?> qualifier) throws IdGenerationException {
        return getNextId(qualifier.getName());
    }

    /**
     * {@inheritDoc}
     */
    public void reset(final String qualifier) {
        idGenerators.get(qualifier).reset();
    }

    /**
     * {@inheritDoc}
     */
    public void reset(final Class<?> qualifier) {
        idGenerators.get(qualifier).reset();
    }

    /**
     * Sets the {@link SequenceGenerationDao}
     * 
     * @param sequenceGenerationDao
     *            the new {@link SequenceGenerationDao}
     */
    public void setSequenceGenerationDao(final SequenceGenerationDao sequenceGenerationDao) {
        this.sequenceGenerationDao = sequenceGenerationDao;
    }

    /**
     * Sets the qualifiers.
     * 
     * @param qualifiers
     *            the new qualifiers
     */
    public void setQualifiers(final Properties qualifiers) {
        this.qualifiers = qualifiers;
    }

    /**
     * Inner class - an instance of which is created for each persistent entity that requires ID generation for its primary key
     */
    private class IdGenerator {

        private long available;
        private final AtomicLong nextId = new AtomicLong();
        private final Object mutex = new Object();
        private final String generationSql;
        private long batchSize = 1L;

        private IdGenerator(final String sequenceMetadata) {
            final String[] temp = sequenceMetadata.split(":");
            final String sequenceName = temp[0];
            if (temp.length == 2) {
                batchSize = Long.parseLong(temp[1]);
            }
            generationSql = String.format("select %s.nextval from dual", sequenceName);
        }

        public Long getNextId() throws IdGenerationException {
            synchronized (mutex) {
                if (nextId.get() == 0 || available == nextId.get()) {
                    nextId.set(sequenceGenerationDao.getNextID(generationSql));
                    available = nextId.get() + batchSize;
                }
                return nextId.getAndIncrement();
            }
        }

        /**
         * This method resets the sequence generator.
         */
        public void reset() {
            synchronized (mutex) {
                available = 0;
                nextId.set(0);
            }
        }

    }

}
